/*
 * Package il.ac.biu.cs.grossmm.impl.simple
 * File SipPublishAdapter.java
 * 
 * This is a part of presence service framework prototype. 
 * See javadoc for more information.
 *  
 * Copyright (C) 2006 Grossmann Mark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package il.ac.biu.cs.grossmm.impl.simple;

import static il.ac.biu.cs.grossmm.api.data.SetOf.setOf;
import static il.ac.biu.cs.grossmm.api.keys.KeyFactory.entry;
import static il.ac.biu.cs.grossmm.api.keys.KeyFactory.key;
import static il.ac.biu.cs.grossmm.api.presence.BaseVocabulary.RESOURCE_TYPE;
import il.ac.biu.cs.grossmm.api.OperationFailedException;
import il.ac.biu.cs.grossmm.api.data.Node;
import il.ac.biu.cs.grossmm.api.data.Nodeset;
import il.ac.biu.cs.grossmm.api.data.Root;
import il.ac.biu.cs.grossmm.api.data.SetOf;
import il.ac.biu.cs.grossmm.api.flow.DataProcessorBase;
import il.ac.biu.cs.grossmm.api.flow.NodeManipulator;
import il.ac.biu.cs.grossmm.api.flow.PublicationPoint;
import il.ac.biu.cs.grossmm.api.keys.Key;
import il.ac.biu.cs.grossmm.api.presence.Entity;
import il.ac.biu.cs.grossmm.api.presence.Registrar;
import il.ac.biu.cs.grossmm.api.presence.ResourceType;
import il.ac.biu.cs.grossmm.api.presence.XomRpid;
import il.ac.biu.cs.grossmm.api.presence.XomRpidDevice;
import il.ac.biu.cs.grossmm.api.presence.XomRpidPerson;
import il.ac.biu.cs.grossmm.api.presence.XomRpidTuple;
import il.ac.biu.cs.grossmm.api.server.ComponentManager;
import il.ac.biu.cs.grossmm.api.server.ServiceManager;
import il.ac.biu.cs.grossmm.api.simple.SipRequestProcessor;
import il.ac.biu.cs.grossmm.api.simple.SipSharedProvider;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.SipFactory;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.EventHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.Header;
import javax.sip.header.HeaderFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;

import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.ParsingException;
import nu.xom.ValidityException;

import org.apache.log4j.Logger;

import util.Identifiers;

/**
 * SipPublishAdapter
 */
public class SipPublishAdapter extends DataProcessorBase implements
		SipRequestProcessor {

	public SipPublishAdapter() {
		super("Sip Publication Adapter", true);
	}

	// Log4j
	Logger logger = Logger.getLogger(SipPublishAdapter.class);

	SipFactory sipFactory = null;

	SipStack sipStack = null;

	Registrar registrar;

	Lock pubLock = new ReentrantLock();

	Map<String, Publications> allPubs = new HashMap<String, Publications>();

	PublicationPoint<SetOf<XomRpid>> pp;

	private SipSharedProvider provider;

	final static Nodeset<SetOf<XomRpid>, XomRpid> ns = setOf(XomRpid.class).NODESET;

	private static final String PIDF_URI = "urn:ietf:params:xml:ns:pidf";

	private static final String RPID_URI = "urn:ietf:params:xml:ns:pidf:rpid";

	private static final String DM_URI = "urn:ietf:params:xml:ns:pidf:data-model";

	private HeaderFactory headerFactory;

	Builder parser = new Builder();

	public void setComponentManager(ComponentManager container)
			throws Exception {
		provider = container.getByClass(SipSharedProvider.class);
		registrar = container.getByClass(Registrar.class);
		headerFactory = SipFactory.getInstance().createHeaderFactory();
	}

	public void start(ServiceManager serviceManager) throws Exception {

		provider.addSipListener(this);
	}

	public void stop(Exception exception) {
		provider.removeSipListener(this);
	}

	public void terminated(Object serviceHint, Exception e) {
		logger.warn("Terminated: " + serviceHint + " (" + e.getMessage() + ")");
	}

	public void aborted(Object hint, Exception e) throws Exception {
		logger.warn("Aborted: " + hint + " (" + e.getMessage() + ")");

		if (hint.equals(SipSharedProvider.class))
			throw e;
	}

	/**
	 * Process request from UAC (PUA)
	 * 
	 * @param event
	 */
	public synchronized Response processRequest(RequestEvent event) {
		Request request = event.getRequest();

		String method = request.getMethod();

		// accept only publications
		if (!method.equals("PUBLISH")) { // we are strict about caps
			return null;
		}

		if (logger.isDebugEnabled())
			logger.debug("New PUBLISH request received");

		/*
		 * 1. The ESC inspects the Request-URI to determine whether this request
		 * is targeted to a resource for which the ESC is responsible for
		 * maintaining event state. If not, the ESC MUST return a 404 (Not
		 * Found) response and skip the remaining steps.
		 * 
		 * It may also be that the Request-URI points to a domain that the ESC
		 * is not responsible for. In that case, the UAS receiving the request
		 * can assume the role of a proxy server and forward the request to a
		 * more appropriate target.
		 */

		// we accept any Request-URI
		// Obtain presentity/resource uri
		URI uri = request.getRequestURI();
		String scheme = uri.getScheme();

		// check scheme
		if (!scheme.equals("sip") && !scheme.equals("sips")) {
			// TODO: more schemes
			// Return a 416 (RFC3261, 8.2.2.1)
			return provider.createResponse(Response.UNSUPPORTED_URI_SCHEME,
					scheme);
		}

		SipURI sipUri = ((SipURI) uri);
		String user = sipUri.getUser();

		// obtain presentity
		Entity presentity;
		try {
			presentity = registrar.getEntity(user);
		} catch (Exception e) {
			e.printStackTrace();
			//logger.warn(e);
			return provider.createResponse(Response.SERVER_INTERNAL_ERROR,
					"Could not query presentity: " + e.getMessage());
		}

		if (presentity == null) {
			logger.debug("presentity == null");

			// no such presentity, return a 404 (RFC3903, 6, case 1)
			return provider.createResponse(Response.NOT_FOUND, null);
		} else if (logger.isDebugEnabled())
			logger.debug("Presentity: " + presentity);

		/*
		 * 2. The ESC examines the Event header field of the PUBLISH request. If
		 * the Event header field is missing or contains an event package which
		 * the ESC does not support, the ESC MUST respond to the PUBLISH request
		 * with a 489 (Bad Event) response, and skip the remaining steps.
		 */

		EventHeader eventHeader = (EventHeader) request
				.getHeader(EventHeader.NAME);

		if (eventHeader == null) {
			logger.warn("No event header in PUBLISH request");
			return provider.createResponse(Response.BAD_EVENT,
					"Missing event header");
		}

		if (!eventHeader.getEventType().equals("presence")) {
			logger.warn("We do not support event type "
					+ eventHeader.getEventType());
			return provider.createResponse(Response.BAD_EVENT, ""
					+ eventHeader.getEventType() + " not supported");
		}

		/*
		 * The ESC processes the Expires header field value from the PUBLISH
		 * request.
		 * 
		 * If the request has an Expires header field, that value MUST be taken
		 * as the requested expiration.
		 * 
		 * Else, a locally-configured default value MUST be taken as the
		 * requested expiration.
		 * 
		 * The ESC MAY choose an expiration less than the requested expiration
		 * interval. Only if the requested expiration interval is greater than
		 * zero and less than a locally-configured minimum, the ESC MAY reject
		 * the publication with a response of 423 (Interval Too Brief), and skip
		 * the remaining steps. This response MUST contain a Min-Expires header
		 * field that states the minimum expiration interval the ESC is willing
		 * to honor.
		 */

		ExpiresHeader expiresHeader = (ExpiresHeader) request
				.getHeader(ExpiresHeader.NAME);

		int expires = 3600;

		if (expiresHeader != null) {
			expires = expiresHeader.getExpires();
		}

		long timeoutTime = System.currentTimeMillis() + expires * 1000;

		/*
		 * 3. The ESC examines the SIP-If-Match header field of the PUBLISH
		 * request for the presence of a request precondition.
		 */

		Header etagHeader = request.getHeader("SIP-If-Match");

		Object content = request.getContent();

		Root<SetOf<XomRpid>> root;
		Node<XomRpid> rpid;
		NodeManipulator man;
		Node lockNode = null;

		Publications pubs = allPubs.get(presentity);

		if (etagHeader == null) {

			// initial publication?
			logger.debug("No SIP-If-Match header");

			/*
			 * If the request has no message body and contained no entity-tag,
			 * the ESC SHOULD reject the request with an appropriate response,
			 * such as 400 (Invalid Request), and skip the remainder of the
			 * steps. Alternatively, in case either ESC local policy or the
			 * event package has defined semantics for an initial publication
			 * containing no message body, the ESC MAY accept it.
			 */

			if (content == null) {
				logger.warn("No content and no etag");
				return provider.createResponse(Response.BAD_REQUEST,
						"Mising content and etag");
			}

			/*
			 * If the request does not contain a SIP-If-Match header field, the
			 * ESC MUST generate and store a locally unique entity-tag for
			 * identifying the publication. This entity-tag is associated with
			 * the event-state carried in the body of the PUBLISH request.
			 */

			// return (new Publication(presentity)).create(request, alarmTime);
			try {
				if (pubs == null) {
					Key key = key(presentity, entry(RESOURCE_TYPE,
							ResourceType.PRESENCE));
					root = pp.createNode(key, true);

					man = pp.writeLock(lockNode = root);
					rpid = man.createSubnode(root, ns);

					pubs = new Publications(root, presentity);
					allPubs.put(user, pubs);
				} else {
					root = pubs.root;

					man = pp.writeLock(lockNode = root);
					rpid = man.createSubnode(root, ns);
				}
			} catch (OperationFailedException oae) {
				logger.equals(oae);
				return provider.createResponse(Response.SERVER_INTERNAL_ERROR,
						oae.getMessage());
			}

			new Publication(pubs, rpid, timeoutTime);
		} else {
			logger.debug("SIP-If-Match: " + etagHeader.toString());

			/*
			 * Else, if the request has a SIP-If-Match header field, the ESC
			 * checks whether the header field contains a single entity-tag. If
			 * not, the request is invalid, and the ESC MUST return with a 400
			 * (Invalid Request) response and skip the remaining steps.
			 */

			String hvalue = etagHeader.toString();
			int k = hvalue.indexOf(':');
			String etag = null;

			if (k > 0)
				etag = hvalue.substring(k + 1).trim();

			if (etag == null || etag.length() == 0) {
				return provider
						.createResponse(Response.BAD_REQUEST, "Bad etag");
			}

			logger.debug("etag: " + etag);

			/*
			 * Else, the ESC extracts the entity-tag contained in the SIP-If-
			 * Match header field and matches that entity-tag against all
			 * locally stored entity-tags for this resource and event package.
			 * If no match is found, the ESC MUST reject the publication with a
			 * response of 412 (Conditional Request Failed), and skip the
			 * remaining steps.
			 */

			Publication publication = pubs.get(etag);

			if (publication == null) {
				logger.debug("etag: " + etag + " does not match anything");
				return provider.createResponse(412,
						"Conditional Request Failed");
			}

			logger.debug("etag: " + etag + " matches");

			rpid = publication.node;

			/*
			 * the event state identified by the entity-tag is refreshed,
			 * setting the expiration value to the chosen expiration interval.
			 */

			/*
			 * If the chosen expiration interval has a special value of "0", the
			 * event state identified by the entity-tag MUST be immediately
			 * removed. The ESC MUST NOT store any event state as a result of
			 * such a request.
			 */
			if (expires == 0) {

				pubs.remove(publication);

				try {
					if (pubs.isEmpty())
						pp.deleteNode(pubs.root.getKey());
					else {
						man = pp.writeLock(pubs.root);
						man.removeSubnode(rpid);
						pp.writeUnlock(pubs.root);
					}
				} catch (OperationFailedException oae) {
					logger.equals(oae);
					return provider.createResponse(Response.OK, oae
							.getMessage());
				}

				Response resp = provider.createResponse(Response.OK, "removed");
				return resp;

			} else if (content == null) {
				publication.refresh(timeoutTime);
				Response resp = provider.createResponse(Response.OK,
						"Refreshed");
				return resp;
			}

			publication.refresh(timeoutTime);
			rpid = publication.node;
			try {
				man = pp.writeLock(lockNode = rpid);
			} catch (OperationFailedException e) {
				logger.equals(e);
				return provider.createResponse(Response.SERVER_INTERNAL_ERROR,
						e.getMessage());
			}
		}

		// check whether this is a new publication or a publication
		// refresh/update

		/*
		 * The processing of the PUBLISH request MUST be atomic. If internal
		 * errors (such as the inability to access a back-end database) occur
		 * before processing is complete, the publication MUST NOT succeed, and
		 * the ESC MUST fail with an appropriate error response, such as 504
		 * (Server Time-out), and skip the last step.
		 * 
		 * 6. The ESC returns a 200 (OK) response. The response MUST contain an
		 * Expires header field indicating the expiration interval chosen by the
		 * ESC. The response MUST also contain a SIP-ETag header field that
		 * contains a single entity-tag identifying the publication. The ESC
		 * MUST generate a new entity-tag for each successful publication,
		 * replacing any previous entity-tag associated with that event state.
		 * The generated entity-tag MUST be unique from any other entity-tags
		 * currently assigned to event state associated with that Request-URI,
		 * and MUST be different from any entity-tag assigned previously to
		 * event state for that Request-URI. See Section 8.3 for more
		 * information on the ESC handling of entity- tags.
		 */

		Response resp;
		try {
			resp = updateNode(rpid, man, content);
		} catch (OperationFailedException e) {
			pp.writeUnlock(lockNode);
			return provider.createResponse(Response.SERVER_INTERNAL_ERROR, 
					"Could not update presence: "+e.getMessage());
		}

		pp.writeUnlock(lockNode);

		if (resp != null)
			return resp;
		else
			return provider.createResponse(Response.OK, "Published");
	}

	private Response updateNode(Node<XomRpid> rpid, NodeManipulator man,
			Object content) throws OperationFailedException {
		byte[] bytes = (byte[]) content;

		ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

		Document doc;
		try {
			doc = parser.build(bais);
		} catch (ValidityException e) {
			return provider.createResponse(Response.BAD_REQUEST,
					"Non-valid rpid document: " + e.getMessage());
		} catch (ParsingException e) {
			return provider.createResponse(Response.BAD_REQUEST,
					"Rpid document is not well-formed: " + e.getMessage());
		} catch (IOException e) {
			return provider.createResponse(Response.SERVER_INTERNAL_ERROR,
					"Could not parse rpid, I/O error: " + e.getMessage());
		}

		Element rpidRoot = doc.getRootElement();

		if (!rpidRoot.getLocalName().equals("presence"))
			return provider.createResponse(Response.BAD_REQUEST,
					"Root element should be 'presence'");

		if (!rpidRoot.getNamespaceURI().equals(PIDF_URI))
			return provider.createResponse(Response.BAD_REQUEST,
					"Root element URI should be " + PIDF_URI);

		// get all <dm:person> and <dm:device> elements
		Elements dm_children = rpidRoot.getChildElements(null, DM_URI);

		int size = dm_children.size();
		for (int i = 0; i < size; i++) {
			Element element = dm_children.get(i);

			if (element.getLocalName().equals("person")) {
				String id = element.getAttributeValue("id");
				if (id == null)
					return provider.createResponse(Response.BAD_REQUEST,
							"missing id in <person>");

				Node<XomRpidPerson> person = man.getSubnode(rpid,
						XomRpid.PERSONS, XomRpidPerson.ID, id);
				if (person == null) {
					person = man.createSubnode(rpid, XomRpid.PERSONS);
					man.setValue(person, XomRpidPerson.ID, id);
					man.setValue(person, XomRpidPerson.PERSON, element);
				} else {
					man.setAndCheck(person, XomRpidPerson.PERSON, element);
				}
			} else if (element.getLocalName().equals("device")) {
				String id = element.getAttributeValue("id");
				if (id == null)
					return provider.createResponse(Response.BAD_REQUEST,
							"missing id in <device>");

				Node<XomRpidDevice> device = man.getSubnode(rpid,
						XomRpid.DEVICES, XomRpidDevice.ID, id);
				if (device == null) {
					device = man.createSubnode(rpid, XomRpid.DEVICES);
					man.setValue(device, XomRpidDevice.ID, id);
					man.setValue(device, XomRpidDevice.DEVICE, element);
				} else {
					man.setAndCheck(device, XomRpidDevice.DEVICE, element);
				}
			} else
				return provider.createResponse(Response.BAD_REQUEST,
				"Invalid element: "+element);
		}

		Elements pidf_children = rpidRoot.getChildElements(null, PIDF_URI);
		
		size = pidf_children.size();
		for (int i = 0; i < size; i++) {
			Element element = pidf_children.get(i);
			if( element.getLocalName().equals("tuple") ) {
				String id = element.getAttributeValue("id");
				if (id == null)
					return provider.createResponse(Response.BAD_REQUEST,
							"missing id in <tuple>");

				Node<XomRpidTuple> tuple = man.getSubnode(rpid,
						XomRpid.TUPLES, XomRpidTuple.ID, id);
				if (tuple == null) {
					tuple = man.createSubnode(rpid, XomRpid.TUPLES);
					man.setValue(tuple, XomRpidTuple.ID, id);
					man.setValue(tuple, XomRpidTuple.TUPLE, element);
				} else {
					man.setAndCheck(tuple, XomRpidTuple.TUPLE, element);
				}
			}
		}
		
		return null;
	}

	public boolean processResponse(ResponseEvent event) {
		// We do nt send and requests to the UAC/PUA and therefore should not
		// receive any responses
		return false;
	}

	public boolean processTimeout(TimeoutEvent event) {
		// TODO: Do we need this?
		return false;
	}

	static class Publications extends ConcurrentHashMap<String, Publication> {

		private static final long serialVersionUID = -3687374576255529646L;

		// target presentity
		final Entity presentity;

		final Root<SetOf<XomRpid>> root;

		public Publications(Root<SetOf<XomRpid>> root, Entity presentity) {
			this.root = root;
			this.presentity = presentity;
		}
	}

	/**
	 * A SIP publication associated with an entity tag
	 */
	static class Publication {
		/**
		 * Logger for this class
		 */
		private final Logger logger = Logger.getLogger(Publication.class);

		final Publications publications;

		final Node<XomRpid> node;

		long timeoutTime;

		// publication e-tag
		String etag;

		Publication(Publications publications, Node<XomRpid> node,
				long timeoutTime) {
			if (logger.isDebugEnabled())
				logger.debug("Publication() - start");

			this.publications = publications;
			this.node = node;
			this.timeoutTime = timeoutTime;
			changeEtag();

			if (logger.isDebugEnabled())
				logger.debug("Publication() - end");
		}

		void refresh(long timeoutTime) {
			if (logger.isDebugEnabled()) {
				logger.debug("refresh() - start");
			}

			this.timeoutTime = timeoutTime;

			if (logger.isDebugEnabled()) {
				logger.debug("refresh() - end");
			}
		}

		/**
		 * Generate new etag for this publications
		 */
		private void changeEtag() {
			do {
				etag = Identifiers.generate32bitId();
				logger.debug("Publication etag generated: " + etag);
			} while (publications.putIfAbsent(etag, this) != null);
		}

	}

}
